/*
 * File     : StructureField.java
 * Created  : 1 May 2011
 *
 * Copyright © 2011 Matthew Wilson (mj. {my-surname} .uk {at} gmail.com)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.googlecode.dni.internal.structure;

import static com.googlecode.dni.internal.DniTypes.STRUCTURE_HELPER_TYPE;
import static org.objectweb.asm.Opcodes.*;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

import com.googlecode.dni.type.nativetype.NativeChar;
import com.googlecode.dni.type.nativetype.NativeInt;
import com.googlecode.dni.type.nativetype.NativeLong;
import com.googlecode.dni.type.nativetype.NativeShort;
import com.googlecode.dni.type.nativetype.SizeT;
import com.googlecode.dni.type.nativetype.WCharT;
import com.googlecode.dni.type.structure.IllegalStructureException;


/**
 * Encapsulates details of a structure's field used during class generation.
 *
 * @author Matthew Wilson
 */
abstract class StructureField
{

    /** The natively sized types' annotations. */
    static final List< Class< ? extends Annotation > > NATIVE_SIZED_TYPES =
        new ArrayList< Class< ? extends Annotation > >();

    static
    {
        NATIVE_SIZED_TYPES.add( NativeChar.class );
        NATIVE_SIZED_TYPES.add( NativeShort.class );
        NATIVE_SIZED_TYPES.add( NativeInt.class );
        NATIVE_SIZED_TYPES.add( NativeLong.class );
        NATIVE_SIZED_TYPES.add( SizeT.class );
        NATIVE_SIZED_TYPES.add( WCharT.class );
    }


    private Method getter;

    private Method setter;

    private Method pointerSetter;

    private String name;

    private int index;

    private int offset = -1;

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString()
    {
        StringBuilder str = new StringBuilder();
        str.append( getClass().getSimpleName() );
        str.append( "[#" );
        str.append( this.index ).append( " @" );
        str.append( this.offset ).append( ' ' );
        str.append( getSize() ).append( ' ' );
        str.append( this.name ).append( ' ' );
        str.append( getFieldType() ).append( ' ' );
        str.append( getCType() );
        str.append( "]" );
        return str.toString();
    }

    /**
     * @return <code>true</code> iff this type requires a setter
     */
    boolean expectsSetter()
    {
        return true;
    }

    /**
     * @return <code>true</code> iff this type allows a setX(Pointer) method
     */
    boolean allowsAdditionalSetPointer()
    {
        return false;
    }

    /**
     * @return the field type
     */
    Class< ? > getFieldType()
    {
        return this.getter.getReturnType();
    }

    /**
     * @return the getter method
     */
    final Method getGetter()
    {
        return this.getter;
    }

    /**
     * @param getter the getter method
     */
    void setGetter( final Method getter )
    {
        this.getter = getter;
    }

    /**
     * @return the setter method
     */
    final Method getSetter()
    {
        return this.setter;
    }

    /**
     * @param setter the setter method
     */
    void setSetter( final Method setter )
    {
        checkSetter( setter );
        this.setter = setter;
    }

    /**
     * Checks the given candidate method.
     *
     * @param method
     *            the candidate method for a setter
     */
    void checkSetter( final Method method )
    {
        java.lang.reflect.Type[] parameterTypes = method.getGenericParameterTypes();
        if ( parameterTypes.length != 1 )
        {
            throw new IllegalArgumentException( "Structure setter must take one parameter: " + method );
        }

        if ( !this.getter.getGenericReturnType().equals( parameterTypes[ 0 ] ) )
        {
            throw new IllegalArgumentException( "Structure setter / getter types do not agree: " + method );
        }
    }

    /**
     * @return the optional pointer setter method
     */
    Method getPointerSetter()
    {
        return this.pointerSetter;
    }

    /**
     * @param pointerSetter the optional pointer setter method
     */
    void setPointerSetter( final Method pointerSetter )
    {
        this.pointerSetter = pointerSetter;
    }

    /**
     * @return the name
     */
    final String getName()
    {
        return this.name;
    }

    /**
     * @param name the name
     */
    void setName( final String name )
    {
        this.name = name;
    }

    /**
     * @return the index
     */
    int getIndex()
    {
        return this.index;
    }

    /**
     * @param index the index
     */
    void setIndex( final int index )
    {
        this.index = index;
    }

    /**
     * @return the field's offset in bytes
     */
    int getOffset()
    {
        return this.offset;
    }

    /**
     * @param offset the offset to set
     */
    void setOffset( final int offset )
    {
        if ( offset < 0 )
        {
            throw new IllegalArgumentException( "Illegal offset: " + offset );
        }
        this.offset = offset;
    }

    /**
     * Writes any custom fields.
     *
     * @param classWriter
     *            the class writer
     * @param implName
     *            the type implementation name
     */
    @SuppressWarnings( "unused" )
    void writeFields( final ClassWriter classWriter,
                      final String implName )
    {
        // nothing
    }

    /**
     * Writes a fragment for the constructor (to set any custom fields).
     *
     * @param methodVisitor
     *            the method visitor
     * @param implName
     *            the type implementation name
     */
    @SuppressWarnings( "unused" )
    void writeConstructorFragment( final MethodVisitor methodVisitor,
                                   final String implName )
    {
        // nothing
    }

    /**
     * Writes appropriate methods for the structure field.
     *
     * @param classWriter
     *            the class writer
     * @param implName
     *            the type implementation name
     */
    void writeMethods( final ClassWriter classWriter,
                       final String implName )
    {
        if ( this.getter != null )
        {
            // TODO generic types
            MethodVisitor methodVisitor =
                classWriter.visitMethod( ACC_PUBLIC,
                                         this.getter.getName(),
                                         Type.getMethodDescriptor( this.getter ),
                                         null,
                                         null );
            methodVisitor.visitCode();
            writeGetter( methodVisitor, implName );
            methodVisitor.visitMaxs( 0, 0 );
            methodVisitor.visitEnd();
        }

        if ( this.setter != null )
        {
            // TODO generic types
            MethodVisitor methodVisitor =
                classWriter.visitMethod( ACC_PUBLIC,
                                         getSetter().getName(),
                                         Type.getMethodDescriptor( this.setter ),
                                         null,
                                         null );
            methodVisitor.visitCode();
            writeSetter( methodVisitor, implName );
            methodVisitor.visitMaxs( 0, 0 );
            methodVisitor.visitEnd();
        }

        // TODO optional additional setter

    }

    // TODO should these three methods actually be on PrimitiveStructureField?

    /**
     * Writes a test for an equals methods.
     *
     * @param methodVisitor
     *            the method visitor
     * @param returnFalseLabel
     *            the label to jump to if the test fails
     * @param implName
     *            the implementation type name
     */
    void writeEqualsFragment( final MethodVisitor methodVisitor,
                              final Label returnFalseLabel,
                              final String implName )
    {
        String getterDesc = org.objectweb.asm.Type.getMethodDescriptor( this.getter );

        methodVisitor.visitVarInsn( ALOAD, 2 );
        methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                       implName,
                                       "get" + getName(),
                                       getterDesc );

        methodVisitor.visitVarInsn( ALOAD, 0 );
        methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                       implName,
                                       "get" + getName(),
                                       getterDesc );

        Class< ? > fieldType = this.getter.getReturnType();
        if ( fieldType == boolean.class
             || fieldType == byte.class
             || fieldType == short.class
             || fieldType == int.class
             || fieldType == char.class )
        {
            methodVisitor.visitJumpInsn( IF_ICMPNE, returnFalseLabel );
        }
        else if ( fieldType == long.class )
        {
            methodVisitor.visitInsn( LCMP );
            methodVisitor.visitJumpInsn( IFNE, returnFalseLabel );
        }
        else if ( fieldType == float.class )
        {
            // helper method -- too lazy to write out the byte-code for
            // dealing with NaNs
            methodVisitor.visitMethodInsn( INVOKESTATIC,
                                           STRUCTURE_HELPER_TYPE,
                                           "equals",
                                           "(FF)Z" );
            methodVisitor.visitJumpInsn( IFEQ, returnFalseLabel );
        }
        else if ( fieldType == double.class )
        {
            // helper method -- too lazy to write out the byte-code for
            // dealing with NaNs
            methodVisitor.visitMethodInsn( INVOKESTATIC,
                                           STRUCTURE_HELPER_TYPE,
                                           "equals",
                                           "(DD)Z" );
            methodVisitor.visitJumpInsn( IFEQ, returnFalseLabel );
        }
        else
        {
            // helper method
            methodVisitor.visitMethodInsn( INVOKESTATIC,
                                           STRUCTURE_HELPER_TYPE,
                                           "equals",
                                           "(Ljava/lang/Object;Ljava/lang/Object;)Z" );
            methodVisitor.visitJumpInsn( IFEQ, returnFalseLabel );
        }
    }

    /**
     * Writes out code to put the hash-code of this field onto the stack.
     *
     * @param methodVisitor
     *            the method visitor
     * @param implName
     *            the implementation type name
     */
    void writeHashCodeFragment( final MethodVisitor methodVisitor,
                                final String implName )
    {
        String getterDesc = org.objectweb.asm.Type.getMethodDescriptor( this.getter );

        methodVisitor.visitVarInsn( ALOAD, 0 );
        methodVisitor.visitMethodInsn( INVOKEVIRTUAL, implName, "get" + this.name, getterDesc );

        Class< ? > returnType = this.getter.getReturnType();
        if ( returnType != boolean.class && returnType != byte.class && returnType != short.class
             && returnType != int.class && returnType != char.class )
        {
            //  lots of helper methods...
            String sig;
            if ( returnType == long.class )
            {
                sig = "(J)I";
            }
            else if ( returnType == float.class )
            {
                sig = "(F)I";
            }
            else if ( returnType == double.class )
            {
                sig = "(D)I";
            }
            else
            {
                sig = "(Ljava/lang/Object;)I";
            }

            methodVisitor.visitMethodInsn( INVOKESTATIC, STRUCTURE_HELPER_TYPE, "hashCode", sig );
        }
    }

    /**
     * Writes out code to append a string form of this field.
     *
     * @param methodVisitor
     *            the method visitor
     * @param implName
     *            the implementation type name
     */
    void writeToStringFragment( final MethodVisitor methodVisitor,
                                final String implName )
    {
        methodVisitor.visitVarInsn( Opcodes.ALOAD, 0 );
        methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                       implName,
                                       "get" + this.name,
                                       org.objectweb.asm.Type.getMethodDescriptor( this.getter ) );

        Class< ? > returnType = this.getter.getReturnType();
        String sig;
        if ( returnType == byte.class || returnType == short.class || returnType == int.class || returnType == char.class )
        {
            sig = "(I)Ljava/lang/StringBuilder;";
        }
        else if ( returnType.isPrimitive() )
        {
            sig = "(" + org.objectweb.asm.Type.getDescriptor( returnType ) + ")Ljava/lang/StringBuilder;";
        }
        else
        {
            sig = "(Ljava/lang/Object;)Ljava/lang/StringBuilder;";
        }

        methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                       "java/lang/StringBuilder",
                                       "append",
                                       sig );
    }

    /**
     * <p>
     *  Performs any post-creation actions.
     * </p>
     */
    void postCreate()
    {
        // nothing
    }

    /**
     * @return the C type, for debugging purposes
     */
    abstract String getCType();

    /**
     * @return the size of the field, in bytes, without padding
     */
    abstract int getSize();

    /**
     * Determines the unit size of the field, used to calculate padding.
     *
     * @return the unit size of the field, in bytes, without padding
     */
    int getPaddingUnitSize()
    {
        return getSize();
    }

    /**
     * Writes the getter method.
     *
     * @param methodVisitor
     *            the method writer
     * @param implName
     *            the type implementation name
     */
    abstract void writeGetter( MethodVisitor methodVisitor,
                               String implName );

    /**
     * Writes the setter method.
     *
     * @param methodVisitor
     *            the method writer
     * @param implName
     *            the type implementation name
     */
    abstract void writeSetter( MethodVisitor methodVisitor,
                               String implName );


    /**
     * Checks that the getter has no forbidden annotation types.
     *
     * @param forbiddedAnnotationTypes
     *            the forbidden annotation types
     */
    final void checkAnnotations( final List< Class< ? extends Annotation > > forbiddedAnnotationTypes )
    {
        for ( Class< ? extends Annotation > annotationType : forbiddedAnnotationTypes )
        {
            if ( this.getter.getAnnotation( annotationType ) != null )
            {
                throw new IllegalStructureException( "Meaningless @" + annotationType.getSimpleName() + " annotation: " + this.getter );
            }
        }
    }

    /**
     * Writes instructions to place the pointer to this field on the evaluation
     * stack.
     *
     * @param methodVisitor
     *            the method visitor
     * @param implName
     *            the type implementation name
     */
    final void writeGetPointer( final MethodVisitor methodVisitor,
                                final String implName )
    {
        methodVisitor.visitVarInsn( ALOAD, 0 );
        writeGetPointerFrom( methodVisitor, implName );
    }


    /**
     * Writes instructions to place the pointer to this field (from the object
     * on the evaluation stack) on the evaluation stack.
     *
     * @param methodVisitor
     *            the method visitor
     * @param implName
     *            the type implementation name
     */
    final void writeGetPointerFrom( final MethodVisitor methodVisitor, final String implName )
    {
        methodVisitor.visitFieldInsn( GETFIELD, implName, "pointer", "J" );

        if ( getOffset() != 0 )
        {
            methodVisitor.visitLdcInsn( (long) getOffset() );
            methodVisitor.visitInsn( Opcodes.LADD );
        }
    }

}
